﻿// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
    internal class PageActionInvoker : ResourceInvoker, IActionInvoker
    {
        private readonly IPageHandlerMethodSelector _selector;
        private readonly PageContext _pageContext;
        private readonly ParameterBinder _parameterBinder;
        private readonly ITempDataDictionaryFactory _tempDataFactory;
        private readonly HtmlHelperOptions _htmlHelperOptions;
        private readonly CompiledPageActionDescriptor _actionDescriptor;

        private Dictionary<string, object> _arguments;
        private HandlerMethodDescriptor _handler;
        private PageBase _page;
        private object _pageModel;
        private ViewContext _viewContext;

        private PageHandlerSelectedContext _handlerSelectedContext;
        private PageHandlerExecutingContext _handlerExecutingContext;
        private PageHandlerExecutedContext _handlerExecutedContext;

        public PageActionInvoker(
            IPageHandlerMethodSelector handlerMethodSelector,
            DiagnosticListener diagnosticListener,
            ILogger logger,
            IActionResultTypeMapper mapper,
            PageContext pageContext,
            IFilterMetadata[] filterMetadata,
            PageActionInvokerCacheEntry cacheEntry,
            ParameterBinder parameterBinder,
            ITempDataDictionaryFactory tempDataFactory,
            HtmlHelperOptions htmlHelperOptions)
            : base(
                  diagnosticListener,
                  logger,
                  mapper,
                  pageContext,
                  filterMetadata,
                  pageContext.ValueProviderFactories)
        {
            _selector = handlerMethodSelector;
            _pageContext = pageContext;
            CacheEntry = cacheEntry;
            _parameterBinder = parameterBinder;
            _tempDataFactory = tempDataFactory;
            _htmlHelperOptions = htmlHelperOptions;

            _actionDescriptor = pageContext.ActionDescriptor;
        }

        // Internal for testing
        internal PageActionInvokerCacheEntry CacheEntry { get; }

        private bool HasPageModel => _actionDescriptor.HandlerTypeInfo != _actionDescriptor.PageTypeInfo;

        // Internal for testing
        internal PageContext PageContext => _pageContext;

        /// <remarks>
        /// <see cref="ResourceInvoker"/> for details on what the variables in this method represent.
        /// </remarks>
        protected override async Task InvokeInnerFilterAsync()
        {
            var next = State.PageBegin;
            var scope = Scope.Invoker;
            var state = (object)null;
            var isCompleted = false;

            while (!isCompleted)
            {
                await Next(ref next, ref scope, ref state, ref isCompleted);
            }
        }

        protected override void ReleaseResources()
        {
            if (_pageModel != null && CacheEntry.ReleaseModel != null)
            {
                CacheEntry.ReleaseModel(_pageContext, _pageModel);
            }

            if (_page != null && CacheEntry.ReleasePage != null)
            {
                CacheEntry.ReleasePage(_pageContext, _viewContext, _page);
            }
        }

        protected override Task InvokeResultAsync(IActionResult result)
        {
            // We also have some special initialization we need to do for PageResult.
            if (result is PageResult pageResult)
            {
                // If we used a PageModel then the Page isn't initialized yet.
                if (_viewContext == null)
                {
                    _viewContext = new ViewContext(
                        _pageContext,
                        NullView.Instance,
                        _pageContext.ViewData,
                        _tempDataFactory.GetTempData(_pageContext.HttpContext),
                        TextWriter.Null,
                        _htmlHelperOptions);
                    _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath;
                }

                if (_page == null)
                {
                    _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext);
                }
                pageResult.Page = _page;
                pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
            }

            return base.InvokeResultAsync(result);
        }

        private object CreateInstance()
        {
            if (HasPageModel)
            {
                // Since this is a PageModel, we need to activate it, and then run a handler method on the model.
                _pageModel = CacheEntry.ModelFactory(_pageContext);
                _pageContext.ViewData.Model = _pageModel;

                return _pageModel;
            }
            else
            {
                // Since this is a Page without a PageModel, we need to create the Page before running a handler method.
                _viewContext = new ViewContext(
                    _pageContext,
                    NullView.Instance,
                    _pageContext.ViewData,
                    _tempDataFactory.GetTempData(_pageContext.HttpContext),
                    TextWriter.Null,
                    _htmlHelperOptions);
                _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath;

                _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext);

                if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo)
                {
                    _pageContext.ViewData.Model = _page;
                }

                return _page;
            }
        }

        private HandlerMethodDescriptor SelectHandler()
        {
            return _selector.Select(_pageContext);
        }

        private Task BindArgumentsAsync()
        {
            // Perf: Avoid allocating async state machines where possible. We only need the state
            // machine if you need to bind properties or arguments.
            if (_actionDescriptor.BoundProperties.Count == 0 && (_handler == null || _handler.Parameters.Count == 0))
            {
                return Task.CompletedTask;
            }

            return BindArgumentsCoreAsync();
        }

        private async Task BindArgumentsCoreAsync()
        {
            await CacheEntry.PropertyBinder(_pageContext, _instance);

            if (_handler == null)
            {
                return;
            }

            // We do two separate cache lookups, once for the binder and once for the executor.
            // Reducing it to a single lookup requires a lot of code change with little value.
            PageHandlerBinderDelegate handlerBinder = null;
            for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
            {
                if (object.ReferenceEquals(_handler, _actionDescriptor.HandlerMethods[i]))
                {
                    handlerBinder = CacheEntry.HandlerBinders[i];
                    break;
                }
            }

            await handlerBinder(_pageContext, _arguments);
        }

        private static object[] PrepareArguments(
            IDictionary<string, object> argumentsInDictionary,
            HandlerMethodDescriptor handler)
        {
            if (handler.Parameters.Count == 0)
            {
                return null;
            }

            var arguments = new object[handler.Parameters.Count];
            for (var i = 0; i < arguments.Length; i++)
            {
                var parameter = handler.Parameters[i];

                if (argumentsInDictionary.TryGetValue(parameter.ParameterInfo.Name, out var value))
                {
                    // Do nothing, already set the value.
                }
                else if (!ParameterDefaultValue.TryGetDefaultValue(parameter.ParameterInfo, out value) &&
                    parameter.ParameterInfo.ParameterType.IsValueType)
                {
                    value = Activator.CreateInstance(parameter.ParameterInfo.ParameterType);
                }

                arguments[i] = value;
            }

            return arguments;
        }

        private async Task InvokeHandlerMethodAsync()
        {
            var handler = _handler;
            if (_handler != null)
            {
                var arguments = PrepareArguments(_arguments, handler);

                PageHandlerExecutorDelegate executor = null;
                for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
                {
                    if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i]))
                    {
                        executor = CacheEntry.HandlerExecutors[i];
                        break;
                    }
                }

                Debug.Assert(executor != null, "We should always find a executor for a handler");

                _diagnosticListener.BeforeHandlerMethod(_pageContext, handler, _arguments, _instance);
                _logger.ExecutingHandlerMethod(_pageContext, handler, arguments);

                try
                {
                    _result = await executor(_instance, arguments);
                    _logger.ExecutedHandlerMethod(_pageContext, handler, _result);
                }
                finally
                {
                    _diagnosticListener.AfterHandlerMethod(_pageContext, handler, _arguments, _instance, _result);
                }
            }

            // Pages have an implicit 'return Page()' even without a handler method.
            if (_result == null)
            {
                _logger.ExecutingImplicitHandlerMethod(_pageContext);
                _result = new PageResult();
                _logger.ExecutedImplicitHandlerMethod(_result);
            }

            // Ensure ViewData is set on PageResult for backwards compatibility (For example, Identity UI accesses
            // ViewData in a PageFilter's PageHandlerExecutedMethod)
            if (_result is PageResult pageResult)
            {
                pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
            }
        }

        private Task Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
        {
            switch (next)
            {
                case State.PageBegin:
                    {
                        _instance = CreateInstance();

                        goto case State.PageSelectHandlerBegin;
                    }

                case State.PageSelectHandlerBegin:
                    {
                        _cursor.Reset();

                        _handler = SelectHandler();

                        goto case State.PageSelectHandlerNext;
                    }

                case State.PageSelectHandlerNext:

                    var currentSelector = _cursor.GetNextFilter<IPageFilter, IAsyncPageFilter>();
                    if (currentSelector.FilterAsync != null)
                    {
                        if (_handlerSelectedContext == null)
                        {
                            _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance)
                            {
                                HandlerMethod = _handler,
                            };
                        }

                        state = currentSelector.FilterAsync;
                        goto case State.PageSelectHandlerAsyncBegin;
                    }
                    else if (currentSelector.Filter != null)
                    {
                        if (_handlerSelectedContext == null)
                        {
                            _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance)
                            {
                                HandlerMethod = _handler,
                            };
                        }

                        state = currentSelector.Filter;
                        goto case State.PageSelectHandlerSync;
                    }
                    else
                    {
                        goto case State.PageSelectHandlerEnd;
                    }

                case State.PageSelectHandlerAsyncBegin:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerSelectedContext != null);

                        var filter = (IAsyncPageFilter)state;
                        var handlerSelectedContext = _handlerSelectedContext;

                        _diagnosticListener.BeforeOnPageHandlerSelection(handlerSelectedContext, filter);
                        _logger.BeforeExecutingMethodOnFilter(
                            PageLoggerExtensions.PageFilter,
                            nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync),
                            filter);

                        var task = filter.OnPageHandlerSelectionAsync(handlerSelectedContext);
                        if (task.Status != TaskStatus.RanToCompletion)
                        {
                            next = State.PageSelectHandlerAsyncEnd;
                            return task;
                        }

                        goto case State.PageSelectHandlerAsyncEnd;
                    }

                case State.PageSelectHandlerAsyncEnd:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerSelectedContext != null);

                        var filter = (IAsyncPageFilter)state;

                        _diagnosticListener.AfterOnPageHandlerSelection(_handlerSelectedContext, filter);
                        _logger.AfterExecutingMethodOnFilter(
                            PageLoggerExtensions.PageFilter,
                            nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync),
                            filter);

                        goto case State.PageSelectHandlerNext;
                    }

                case State.PageSelectHandlerSync:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerSelectedContext != null);

                        var filter = (IPageFilter)state;
                        var handlerSelectedContext = _handlerSelectedContext;

                        _diagnosticListener.BeforeOnPageHandlerSelected(handlerSelectedContext, filter);
                        _logger.BeforeExecutingMethodOnFilter(
                            PageLoggerExtensions.PageFilter,
                            nameof(IPageFilter.OnPageHandlerSelected),
                            filter);

                        filter.OnPageHandlerSelected(handlerSelectedContext);

                        _diagnosticListener.AfterOnPageHandlerSelected(handlerSelectedContext, filter);

                        goto case State.PageSelectHandlerNext;
                    }

                case State.PageSelectHandlerEnd:
                    {
                        if (_handlerSelectedContext != null)
                        {
                            _handler = _handlerSelectedContext.HandlerMethod;
                        }

                        _arguments = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

                        _cursor.Reset();

                        var task = BindArgumentsAsync();
                        if (task.Status != TaskStatus.RanToCompletion)
                        {
                            next = State.PageNext;
                            return task;
                        }

                        goto case State.PageNext;
                    }

                case State.PageNext:
                    {
                        var current = _cursor.GetNextFilter<IPageFilter, IAsyncPageFilter>();
                        if (current.FilterAsync != null)
                        {
                            if (_handlerExecutingContext == null)
                            {
                                _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters, _handler, _arguments, _instance);
                            }

                            state = current.FilterAsync;
                            goto case State.PageAsyncBegin;
                        }
                        else if (current.Filter != null)
                        {
                            if (_handlerExecutingContext == null)
                            {
                                _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters, _handler, _arguments, _instance);
                            }

                            state = current.Filter;
                            goto case State.PageSyncBegin;
                        }
                        else
                        {
                            goto case State.PageInside;
                        }
                    }

                case State.PageAsyncBegin:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerExecutingContext != null);

                        var filter = (IAsyncPageFilter)state;
                        var handlerExecutingContext = _handlerExecutingContext;

                        _diagnosticListener.BeforeOnPageHandlerExecution(handlerExecutingContext, filter);
                        _logger.BeforeExecutingMethodOnFilter(
                            PageLoggerExtensions.PageFilter,
                            nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync),
                            filter);

                        var task = filter.OnPageHandlerExecutionAsync(handlerExecutingContext, InvokeNextPageFilterAwaitedAsync);
                        if (task.Status != TaskStatus.RanToCompletion)
                        {
                            next = State.PageAsyncEnd;
                            return task;
                        }

                        goto case State.PageAsyncEnd;
                    }

                case State.PageAsyncEnd:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerExecutingContext != null);

                        var filter = (IAsyncPageFilter)state;

                        if (_handlerExecutedContext == null)
                        {
                            // If we get here then the filter didn't call 'next' indicating a short circuit.
                            _logger.PageFilterShortCircuited(filter);

                            _handlerExecutedContext = new PageHandlerExecutedContext(
                                _pageContext,
                                _filters,
                                _handler,
                                _instance)
                            {
                                Canceled = true,
                                Result = _handlerExecutingContext.Result,
                            };
                        }

                        _diagnosticListener.AfterOnPageHandlerExecution(_handlerExecutedContext, filter);
                        _logger.AfterExecutingMethodOnFilter(
                           PageLoggerExtensions.PageFilter,
                           nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync),
                           filter);

                        goto case State.PageEnd;
                    }

                case State.PageSyncBegin:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerExecutingContext != null);

                        var filter = (IPageFilter)state;
                        var handlerExecutingContext = _handlerExecutingContext;

                        _diagnosticListener.BeforeOnPageHandlerExecuting(handlerExecutingContext, filter);
                        _logger.BeforeExecutingMethodOnFilter(
                           PageLoggerExtensions.PageFilter,
                           nameof(IPageFilter.OnPageHandlerExecuting),
                           filter);

                        filter.OnPageHandlerExecuting(handlerExecutingContext);

                        _diagnosticListener.AfterOnPageHandlerExecuting(handlerExecutingContext, filter);
                        _logger.AfterExecutingMethodOnFilter(
                           PageLoggerExtensions.PageFilter,
                           nameof(IPageFilter.OnPageHandlerExecuting),
                           filter);

                        if (handlerExecutingContext.Result != null)
                        {
                            // Short-circuited by setting a result.
                            _logger.PageFilterShortCircuited(filter);

                            _handlerExecutedContext = new PageHandlerExecutedContext(
                                _pageContext,
                                _filters,
                                _handler,
                                _instance)
                            {
                                Canceled = true,
                                Result = _handlerExecutingContext.Result,
                            };

                            goto case State.PageEnd;
                        }

                        var task = InvokeNextPageFilterAsync();
                        if (task.Status != TaskStatus.RanToCompletion)
                        {
                            next = State.PageSyncEnd;
                            return task;
                        }

                        goto case State.PageSyncEnd;
                    }

                case State.PageSyncEnd:
                    {
                        Debug.Assert(state != null);
                        Debug.Assert(_handlerExecutingContext != null);
                        Debug.Assert(_handlerExecutedContext != null);

                        var filter = (IPageFilter)state;
                        var handlerExecutedContext = _handlerExecutedContext;

                        _diagnosticListener.BeforeOnPageHandlerExecuted(handlerExecutedContext, filter);
                        _logger.BeforeExecutingMethodOnFilter(
                           PageLoggerExtensions.PageFilter,
                           nameof(IPageFilter.OnPageHandlerExecuted),
                           filter);

                        filter.OnPageHandlerExecuted(handlerExecutedContext);

                        _diagnosticListener.AfterOnPageHandlerExecuted(handlerExecutedContext, filter);
                        _logger.AfterExecutingMethodOnFilter(
                           PageLoggerExtensions.PageFilter,
                           nameof(IPageFilter.OnPageHandlerExecuted),
                           filter);

                        goto case State.PageEnd;
                    }

                case State.PageInside:
                    {
                        var task = InvokeHandlerMethodAsync();
                        if (task.Status != TaskStatus.RanToCompletion)
                        {
                            next = State.PageEnd;
                            return task;
                        }

                        goto case State.PageEnd;
                    }

                case State.PageEnd:
                    {
                        if (scope == Scope.Page)
                        {
                            if (_handlerExecutedContext == null)
                            {
                                _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler, _instance)
                                {
                                    Result = _result,
                                };
                            }

                            isCompleted = true;
                            return Task.CompletedTask;
                        }

                        var handlerExecutedContext = _handlerExecutedContext;
                        Rethrow(handlerExecutedContext);

                        if (handlerExecutedContext != null)
                        {
                            _result = handlerExecutedContext.Result;
                        }

                        isCompleted = true;
                        return Task.CompletedTask;
                    }

                default:
                    throw new InvalidOperationException();
            }
        }

        private async Task InvokeNextPageFilterAsync()
        {
            try
            {
                var next = State.PageNext;
                var state = (object)null;
                var scope = Scope.Page;
                var isCompleted = false;
                while (!isCompleted)
                {
                    await Next(ref next, ref scope, ref state, ref isCompleted);
                }
            }
            catch (Exception exception)
            {
                _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler, _instance)
                {
                    ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception),
                };
            }

            Debug.Assert(_handlerExecutedContext != null);
        }

        private async Task<PageHandlerExecutedContext> InvokeNextPageFilterAwaitedAsync()
        {
            Debug.Assert(_handlerExecutingContext != null);
            if (_handlerExecutingContext.Result != null)
            {
                // If we get here, it means that an async filter set a result AND called next(). This is forbidden.
                var message = Resources.FormatAsyncPageFilter_InvalidShortCircuit(
                    typeof(IAsyncPageFilter).Name,
                    nameof(PageHandlerExecutingContext.Result),
                    typeof(PageHandlerExecutingContext).Name,
                    typeof(PageHandlerExecutionDelegate).Name);

                throw new InvalidOperationException(message);
            }

            await InvokeNextPageFilterAsync();

            Debug.Assert(_handlerExecutedContext != null);
            return _handlerExecutedContext;
        }

        private static void Rethrow(PageHandlerExecutedContext context)
        {
            if (context == null)
            {
                return;
            }

            if (context.ExceptionHandled)
            {
                return;
            }

            if (context.ExceptionDispatchInfo != null)
            {
                context.ExceptionDispatchInfo.Throw();
            }

            if (context.Exception != null)
            {
                throw context.Exception;
            }
        }

        private enum Scope
        {
            Invoker,
            Page,
        }

        private enum State
        {
            PageBegin,
            PageSelectHandlerBegin,
            PageSelectHandlerNext,
            PageSelectHandlerAsyncBegin,
            PageSelectHandlerAsyncEnd,
            PageSelectHandlerSync,
            PageSelectHandlerEnd,
            PageNext,
            PageAsyncBegin,
            PageAsyncEnd,
            PageSyncBegin,
            PageSyncEnd,
            PageInside,
            PageEnd,
        }
    }
}
